mysql IS NULL 使用索引
简介
mysql的sql查询语句中使用is null、is not null、!=对索引并没有任何影响,并不会因为where条件中使用了is null、is not null、!=这些判断条件导致索引失效而全表扫描。
mysql官方文档也已经明确说明is null并不会影响索引的使用。
MySQL can perform the same optimization on col_name IS NULL that it can use for col_name = constant_value. For example, MySQL can use indexes and ranges to search for NULL with IS NULL.
事实上,导致索引失效而全表扫描的通常是因为一次查询中回表数量太多。mysql计算认为使用索引的时间成本高于全表扫描,于是mysql宁可全表扫描也不愿意使用索引。
案例
1 | CREATE TABLE `user_info` ( |
1 | INSERT INTO `user_info` (`id`, `name`, `age`) VALUES ('1', 'tom', '18'); |
执行sql查询时使用is null、is not null,发现依然使用的索引查询,并没有出现索引失效的问题。


分析
分析上述现象,则需要详细了解mysql索引的工作原理以及索引数据结构。下面,分别通过工具解析和直接查看二进制文件两种方式分别分析mysql索引数据结构。
工具解析
innodb_ruby是一个非常强大的mysql分析工具,可以用来轻松解析mysql的.ibd文件进而深入理解mysql的数据结构。
首先安装innodb_ruby工具:
1 | yum install -y rubygems ruby-deve |
innodb_ruby的功能很多,此处我们只需要用来解析mysql的索引结构,因此只需要如下的命令即可。更多的功能和命令详见wiki。
1 | innodb_space -s ibdata1 -T sakila/film -I PRIMARY index-recurse |
解析主键索引:
1 | $ innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I PRIMARY index-recurse |
解析普通索引index_name:
1 | $ innodb_space -s /usr/soft/mysql-5.6.31/data -T test/user_info -I index_name index-recurse |
通过解析工具数据mysql的索引结构可以发现,null值也被储存到了索引树中,并且null值被处理成最小的值放在index_name索引树的最左侧。
二进制文件
找到user_info表对应的物理文件user_info.ibd,通过软件例如UltraEdit打开,直接定位到第5个数据页(mysql默认一个数据页占用16KB)。

如图,这些二进制数据就是index_name索引对应的索引页数据,只挑选其中的索引记录,展开如下:
最小记录0x00010063
1 | 01 B2 01 00 02 00 29 记录头信息 |
最大记录0x00010070
1 | 00 04 00 0B 00 00 记录头信息 |
ID为1的索引0x0001007f
1 | 03 00 00 00 10 FF F1 记录头信息 |
ID为2的索引0x0001008c
1 | 01 00 00 18 00 0B 记录头信息 |
ID为3的索引0x00010097
1 | 03 00 00 00 20 FF E8 记录头信息 |
最小记录的记录头信息最后2字节00 29 -> 0x00010063偏移0x0029 -> 0x0001008C,即ID为2的索引位置;
ID为2的记录头信息最后2字节00 0B -> 0x0001008C偏移0x000B -> 0x00010097,即ID为3的索引位置;
ID为3的记录头信息最后2字节FF E8 -> 0x00010097偏移0xFFE8 -> 0x0001007F,即ID为1的索引位置;
ID为1的记录头信息最后2字节FF F1 -> 0x0001007F偏移0xFFF1 -> 0x00010070,最大记录的记录位置;
由此可见索引记录是通过单向链表并以索引值排序串联在一起,而null值被处理成最小的值放在了索引链表的最开始位置,也就是索引树的最左侧。与innodb_ruby工具解析出来的结果一致。
误解原因
为何大众误解认为is null、is not null、!=这些判断条件会导致索引失效而全表扫描呢?
导致索引失效而全表扫描的通常是因为一次查询中回表数量太多。mysql计算认为使用索引的时间成本高于全表扫描,于是mysql宁可全表扫描也不愿意使用索引。使用索引的时间成本高于全表扫描的临界值可以简单得记忆为20%左右。
详细的分析过程可以见笔者的另一篇博客:mysql回表致索引失效。
也就是如果一条查询语句导致的回表范围超过全部记录的20%,则会出现索引失效的问题。而is null、is not null、!=这些判断条件经常会出现在这些回表范围很大的场景,然后被人误解为是这些判断条件导致的索引失效。
复现索引失效
复现索引失效,只需要回表范围超过全部记录的20%,如下插入1000条非null记录。
1 | delimiter // |
此时user_info表中一共有1003条记录,其中只有1条记录的name值为null。那么is null判断语句导致的回表记录只有1/1003不会超过临界值,而is not null判断语句导致的回表记录有1002/1003远远超过临界值,将出现索引失效的现象。
由下两图也可以见,is null依然正常使用索引,而is not null如预期由于回表率太高而宁可全表扫描也不使用索引。


使用mysql的optimizer tracing(mysql5.6版本开始支持)功能来分析sql的执行计划:
1 | SET optimizer_trace="enabled=on"; |
optimizer tracing输出的执行计划可见,该查询下,使用全表扫描所需要的时间成本为206.9;而使用索引所需要的时间成本为1203.4,远远高于全表扫描。因此mysql最终选择全表扫描而出现索引失效的现象。
1 | { |